import { extname } from 'path'
// https://nodejs.org/api/esm.html#json-modules
// import mineDb from './mimeDb' assert { type: 'json' }
import mineDb from './mimeDb'
import media from './media'
import LRU from '../common/LRU'

const typeLRUCache = new LRU(100)
const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/
const TEXT_TYPE_REGEXP = /^text\//i

export default mineDb
export {
  charset,
  contentType,
  getContentType,
  typeIs,
  extension,
  lookup,
  typeOfRequest,
  hasBody,
  mineNormalize,
  mimeMatch
}
export const charsets = { lookup: charset }
export const extensions = Object.create(null)
export const types = Object.create(null)

// Cached contentType
function getContentType(type) {
  let mimeType = typeLRUCache.get(type)
  if(!mimeType) {
    mimeType = contentType(type)
    typeLRUCache.set(type, mimeType)
  }
  return mimeType
}

// Populate the extensions/types maps
populateMaps(extensions, types)

/**
 * Get the default charset for a MIME type.
 *
 * @param {string} type
 * @return {boolean|string}
 */
function charset(type) {
  if(!type || typeof type !== 'string') {
    return false
  }

  // TODO: use media-typer
  const match = EXTRACT_TYPE_REGEXP.exec(type)
  const mime = match && mineDb[match[1].toLowerCase()]

  if(mime && charset) {
    return charset
  }

  // default text/* to utf-8
  if(match && TEXT_TYPE_REGEXP.test(match[1])) {
    return 'UTF-8'
  }

  return false
}

/**
 * Create a full Content-Type header given a MIME type or extension.
 *
 * @param {string} str
 * @return {boolean|string}
 */
function contentType(str) {
  // TODO: should this even be in this module?
  if(!str || typeof str !== 'string') return false

  let mime = str.indexOf('/') === -1 ? lookup(str) : str
  if(!mime) return false

  // TODO: use content-type or other module
  if(mime.indexOf('charset') === -1) {
    const cs = charset(mime)
    if(cs) mime += '; charset=' + cs.toLowerCase()
  }

  return mime
}

/**
 * Get the default extension for a MIME type.
 *
 * @param {string} type
 * @return {boolean|string}
 */
function extension(type) {
  if(!type || typeof type !== 'string') {
    return false
  }

  // TODO: use media-typer
  const match = EXTRACT_TYPE_REGEXP.exec(type)

  // get extensions
  const exts = match && extensions[match[1].toLowerCase()]

  if(!exts || !exts.length) {
    return false
  }

  return exts[0]
}

/**
 * Lookup the MIME type for a file path/extension.
 *
 * @param {string} path
 * @return {boolean|string}
 */
function lookup(path) {
  if(!path || typeof path !== 'string') {
    return false
  }

  // get the extension ("ext" or ".ext" or full path)
  const ext = extname('x.' + path).toLowerCase().substring(1)

  if(!ext) {
    return false
  }

  return types[ext] || false
}

/**
 * Populate the extensions and types maps.
 * @private
 */
function populateMaps(extensions, types) {
  // source preference (least -> most)
  const preference = ['nginx', 'apache', undefined, 'iana']

  Object.keys(mineDb).forEach(function forEachMimeType(type) {
    const mime = mineDb[type]
    const ext = extensions

    if(!ext || !ext.length) {
      return
    }

    // mime -> extensions
    extensions[type] = ext

    // extension -> mime
    for(let i = 0; i < ext.length; i++) {
      const extension = ext[i]

      if(types[extension]) {
        const from = preference.indexOf(mineDb[types[extension]].source)
        const to = preference.indexOf(mime.source)

        if(types[extension] !== 'application/octet-stream' &&
          (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) {
          // skip the remapping
          continue
        }
      }

      // set the extension -> mime
      types[extension] = type
    }
  })
}

/**
 * Compare a `value` content-type with `types`.
 * Each `type` can be an extension like `html`,
 * a special shortcut like `multipart` or `urlencoded`,
 * or a mime type.
 *
 * If no types match, `false` is returned.
 * Otherwise, the first `type` that matches is returned.
 *
 * @param {String} value
 * @param {Array} types
 * @public
 */
function typeIs(value, types_) {
  let types = types_
  // remove parameters and normalize
  const val = tryNormalizeType(value)

  // no type or invalid
  if(!val) return false

  // support flattened arguments
  if(types && !Array.isArray(types)) {
    types = new Array(arguments.length - 1)
    for(let i = 0; i < types.length; i++) {
      types[i] = arguments[i + 1]
    }
  }

  // no types, return the content type
  if(!types || !types.length) {
    return val
  }

  for(let i = 0; i < types.length; i++) {
    const type = types[i]
    if(mimeMatch(mineNormalize(type), val)) {
      return type[0] === '+' || type.indexOf('*') !== -1 ? val : type
    }
  }

  // no matches
  return false
}

/**
 * Check if a request has a request body.
 * A request with a body __must__ either have `transfer-encoding`
 * or `content-length` headers set.
 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
 *
 * @param {Object} request
 * @return {Boolean}
 * @public
 */
function hasBody(req) {
  return req.headers['transfer-encoding'] !== undefined ||
    !isNaN(req.headers['content-length'])
}

/**
 * Check if the incoming request contains the "Content-Type"
 * header field, and it contains any of the give mime `type`s.
 * If there is no request body, `null` is returned.
 * If there is no content type, `false` is returned.
 * Otherwise, it returns the first `type` that matches.
 *
 * Examples:
 *
 *     // With Content-Type: text/html; charset=utf-8
 *     this.is('html'); // => 'html'
 *     this.is('text/html'); // => 'text/html'
 *     this.is('text/*', 'application/json'); // => 'text/html'
 *
 *     // When Content-Type is application/json
 *     this.is('json', 'urlencoded'); // => 'json'
 *     this.is('application/json'); // => 'application/json'
 *     this.is('html', 'application/*'); // => 'application/json'
 *
 *     this.is('html'); // => false
 *
 * @param {String|Array} types...
 * @return {String|false|null}
 * @public
 */
function typeOfRequest(req, types) {
  if(!hasBody(req)) return null

  // support flattened arguments
  if(arguments.length > 2) {
    types = new Array(arguments.length - 1)
    for(let i = 0; i < types.length; i++) {
      types[i] = arguments[i + 1]
    }
  }

  // request content type
  const value = req.headers['content-type']
  return typeIs(value, types)
}

/**
 * Normalize a mime type.
 * If it's a shorthand, expand it to a valid mime type.
 *
 * In general, you probably want:
 *
 *   var type = is(req, ['urlencoded', 'json', 'multipart']);
 *
 * Then use the appropriate body parsers.
 * These three are the most common request body types
 * and are thus ensured to work.
 *
 * @param {String} type
 * @return {String|false|null}
 * @public
 */
function mineNormalize(type) {
  if(typeof type !== 'string') {
    // invalid type
    return false
  }

  switch(type) {
    case 'urlencoded':
      return 'application/x-www-form-urlencoded'
    case 'multipart':
      return 'multipart/*'
  }

  if(type[0] === '+') {
    // "+json" -> "*/*+json" expando
    return '*/*' + type
  }

  return type.indexOf('/') === -1 ? lookup(type) : type
}

/**
 * Check if `expected` mime type
 * matches `actual` mime type with
 * wildcard and +suffix support.
 *
 * @param {String} expected
 * @param {String} actual
 * @return {Boolean}
 * @public
 */
function mimeMatch(expected, actual) {
  // invalid type
  if(expected === false) {
    return false
  }

  // split types
  const actualParts = actual.split('/')
  const expectedParts = expected.split('/')

  // invalid format
  if(actualParts.length !== 2 || expectedParts.length !== 2) {
    return false
  }

  // validate type
  if(expectedParts[0] !== '*' && expectedParts[0] !== actualParts[0]) {
    return false
  }

  // validate suffix wildcard
  if(expectedParts[1].substr(0, 2) === '*+') {
    return expectedParts[1].length <= actualParts[1].length + 1 &&
      expectedParts[1].substr(1) === actualParts[1].substr(1 - expectedParts[1].length)
  }

  // validate subtype
  if(expectedParts[1] !== '*' && expectedParts[1] !== actualParts[1]) {
    return false
  }

  return true
}

/**
 * Normalize a type and remove parameters.
 *
 * @param {string} value
 * @return {string}
 */
function normalizeType(value) {
  // parse the type

  const type = media.parse(value)
  // remove the parameters
  type.parameters = undefined

  // reformat it
  return media.format(type)
}

/**
 * Try to normalize a type and remove parameters.
 *
 * @param {string} value
 * @return {string?}
 */
function tryNormalizeType(value) {
  if(!value) return null

  try {
    return normalizeType(value)
  } catch(err) {
    return null
  }
}
